#include <windows.h>
#include <d3d9.h>
#include <d3dx9.h>
#include <d3dx9math.h>
#include <stdio.h>
#include "dxdiag_res.h"
#include "wine/debug.h"

#define D3DFVF_CUSTOMVERTEX (D3DFVF_XYZ | D3DFVF_TEX1)
#define TEST_TIMEOUT 5000.0
#define XROT_VAL  10.0f
#define YROT_VAL  11.0f
#define ZROT_VAL  12.0f
#define ROT_ANG_FREQ 0.02

WINE_DEFAULT_DEBUG_CHANNEL(dxdiag);

typedef struct
{
    float x,y,z,tu,tv;
} Vertex;

/* Is a test instance currently running? */
volatile int gd3d_running = FALSE;

/* Time variables */
double gd3d_dt;
double gd3d_t_cur;
double gd3d_t_last;
double gd3d_t_elapsed;

/* Handles */
HWND gd3d_hWnd;
LPDIRECT3D9 gd3d_pD3D9;
LPDIRECT3DDEVICE9 gd3d_pDevice;
LPDIRECT3DVERTEXBUFFER9 gd3d_pVertexBuffer;
LPDIRECT3DTEXTURE9 gd3d_pTexture;
LPDIRECT3DSURFACE9 gd3d_pSurface;
D3DLOCKED_RECT gd3d_lRect;

/* Cube vertex data */
Vertex D3DTest_cubeVertices[] =
{
    {-1.0f, 1.0f,-1.0f,  0.0f,0.0f },
    { 1.0f, 1.0f,-1.0f,  1.0f,0.0f },
    {-1.0f,-1.0f,-1.0f,  0.0f,1.0f },
    { 1.0f,-1.0f,-1.0f,  1.0f,1.0f },

    {-1.0f, 1.0f, 1.0f,  1.0f,0.0f },
    {-1.0f,-1.0f, 1.0f,  1.0f,1.0f },
    { 1.0f, 1.0f, 1.0f,  0.0f,0.0f },
    { 1.0f,-1.0f, 1.0f,  0.0f,1.0f },

    {-1.0f, 1.0f, 1.0f,  0.0f,0.0f },
    { 1.0f, 1.0f, 1.0f,  1.0f,0.0f },
    {-1.0f, 1.0f,-1.0f,  0.0f,1.0f },
    { 1.0f, 1.0f,-1.0f,  1.0f,1.0f },

    {-1.0f,-1.0f, 1.0f,  0.0f,0.0f },
    {-1.0f,-1.0f,-1.0f,  1.0f,0.0f },
    { 1.0f,-1.0f, 1.0f,  0.0f,1.0f },
    { 1.0f,-1.0f,-1.0f,  1.0f,1.0f },

    { 1.0f, 1.0f,-1.0f,  0.0f,0.0f },
    { 1.0f, 1.0f, 1.0f,  1.0f,0.0f },
    { 1.0f,-1.0f,-1.0f,  0.0f,1.0f },
    { 1.0f,-1.0f, 1.0f,  1.0f,1.0f },

    {-1.0f, 1.0f,-1.0f,  1.0f,0.0f },
    {-1.0f,-1.0f,-1.0f,  1.0f,1.0f },
    {-1.0f, 1.0f, 1.0f,  0.0f,0.0f },
    {-1.0f,-1.0f, 1.0f,  0.0f,1.0f }
};

void D3DTest_initD3D(void);
void D3DTest_render(void);
void D3DTest_shutdown(void);
void D3DTest_loadTexture(void);

LRESULT CALLBACK D3DTest_DlgProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);

void D3DTest_initD3D(void)
{
    /* Local temporary variables */
    D3DDISPLAYMODE D3Ddm;
    D3DPRESENT_PARAMETERS D3Dpp;
    D3DMATRIX mat_proj;
    void *pVertices = NULL;

    /* Create a new Direct3D object */
    gd3d_pD3D9 = Direct3DCreate9(D3D_SDK_VERSION);

    if (!gd3d_pD3D9)
    {
        WINE_ERR("Direct3DCreate9() - Failed\n");
        abort();
    }

    IDirect3D9_GetAdapterDisplayMode(gd3d_pD3D9, D3DADAPTER_DEFAULT, &D3Ddm);

    /* Fill out presentation parameters */
    ZeroMemory(&D3Dpp, sizeof(D3Dpp));

    D3Dpp.Windowed = TRUE;
    D3Dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
    D3Dpp.BackBufferFormat = D3Ddm.Format;
    D3Dpp.EnableAutoDepthStencil = TRUE;
    D3Dpp.AutoDepthStencilFormat = D3DFMT_D16;
    D3Dpp.PresentationInterval = D3DPRESENT_INTERVAL_IMMEDIATE;

    /* Create the Direct3D device */
    gd3d_pDevice = NULL;
    if (FAILED(IDirect3D9_CreateDevice(gd3d_pD3D9,D3DADAPTER_DEFAULT,D3DDEVTYPE_HAL,
        gd3d_hWnd, D3DCREATE_SOFTWARE_VERTEXPROCESSING, &D3Dpp, &gd3d_pDevice)))
    {
        WINE_ERR("CreateDevice() - Failed\n");
        abort();
    }

    /* Load the texture to use */
    D3DTest_loadTexture();

    /* Create a vertex buffer */
    gd3d_pVertexBuffer = NULL;
    if (FAILED(IDirect3DDevice9_CreateVertexBuffer(gd3d_pDevice, 24 * sizeof(Vertex), 0,
        D3DFVF_CUSTOMVERTEX, D3DPOOL_DEFAULT, &gd3d_pVertexBuffer, NULL)))
    {
        WINE_ERR("CreateVertexBuffer() - Failed\n");
        abort();
    }

    /* Locks a range of vertex data and loads a pointer to vertex buffer memory */
    IDirect3DVertexBuffer9_Lock(gd3d_pVertexBuffer, 0, sizeof(D3DTest_cubeVertices), (void**)&pVertices, 0);
    memcpy(pVertices, D3DTest_cubeVertices, sizeof(D3DTest_cubeVertices));
    IDirect3DVertexBuffer9_Unlock(gd3d_pVertexBuffer);

    /* Disable lighting, enable z-buffer, and set perspective projection */
    IDirect3DDevice9_SetRenderState(gd3d_pDevice, D3DRS_LIGHTING, FALSE);
    IDirect3DDevice9_SetRenderState(gd3d_pDevice, D3DRS_ZENABLE, TRUE);

    D3DXMatrixPerspectiveFovLH(&mat_proj, D3DXToRadian(45.0f), 640.0f / 480.0f, 0.1f, 100.0f); /* TODO: Remove magic #s */
    IDirect3DDevice9_SetTransform(gd3d_pDevice, D3DTS_PROJECTION, &mat_proj);

}

void D3DTest_loadTexture(void)
{
    /* Image parameters (TODO: Replace magic numbers!) */
    DWORD width = 128;
    DWORD height = 128;
    DWORD nPixels = width * height;
    int bpp = 3;
    int offset = sizeof(BITMAPINFO)+1;

    /* Other local variables */
    UINT i;
    BYTE* buffer;
    DWORD* pData;
    HRSRC hResource;
    HGLOBAL hResData;

    /* Get bitmap resource */
    hResource = FindResourceA(GetModuleHandle(0),MAKEINTRESOURCE(IDB_WINE), RT_BITMAP);
    if (!hResource) {
        WINE_ERR("FindResourceA() failed!\n");
        abort();
    }
    
    hResData = LoadResource(GetModuleHandle(0), hResource);
    if (!hResData) {
        WINE_ERR("LoadResource() failed!\n");
        abort();
    }
    buffer = (BYTE*)LockResource(hResData);

    /* Create the texture */
    if (FAILED(IDirect3DDevice9_CreateTexture(gd3d_pDevice, width, height, 8, 0,
        D3DFMT_X8R8G8B8, D3DPOOL_MANAGED, &gd3d_pTexture, 0))) {
        WINE_ERR("CreateTexture() failed!\n");
        abort();
    }
    IDirect3DTexture9_GetSurfaceLevel(gd3d_pTexture,0,&gd3d_pSurface);

    /* Lock texture for copying image */
    IDirect3DTexture9_LockRect(gd3d_pTexture, 0, &gd3d_lRect, 0, D3DLOCK_DISCARD);

    /* Get a pointer into texture buffer data */
    pData = gd3d_lRect.pBits;

    /* Copy over bitmap data into texture buffer */
    for (i = 0; i < nPixels; i++)
        pData[i] = D3DCOLOR_XRGB(buffer[offset+bpp*i],buffer[offset+bpp*i+2],buffer[offset+bpp*i+1]);
        /* Note: bmp file uses RBG instead of RGB! */
        /* From Henri: "That's not actually true, the data is typically stored as BGR(A), but */
        /* "offset" looks wrong. You don't want to parse the bitmap yourself */
        /* though, you should probably be using LoadBitmap() and GetDIBits(). */

    IDirect3DTexture9_UnlockRect(gd3d_pTexture,0);

    /* Set hyperbolic interpolation filters for viewing texture */
    IDirect3DDevice9_SetSamplerState(gd3d_pDevice,0, D3DSAMP_MINFILTER, D3DTEXF_LINEAR);
    IDirect3DDevice9_SetSamplerState(gd3d_pDevice,0, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR);

    FreeResource(hResData);

}

void D3DTest_render(void)
{
    D3DXMATRIX mat_world;
    D3DXMATRIX mat_trans;
    D3DXMATRIX mat_rot;

    static float xrot = 0.0f;
    static float yrot = 0.0f;
    static float zrot = 0.0f;

    static float du;

    du = gd3d_dt * ROT_ANG_FREQ;

    /* Clear the buffer and z-buffer for rendering */
    IDirect3DDevice9_Clear(gd3d_pDevice, 0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, D3DCOLOR_COLORVALUE(0.0f,0.0f,0.0f,1.0f),1.0f,0);

    /* Update cube rotation parameters */
    xrot += XROT_VAL * du;
    yrot += YROT_VAL * du;
    zrot += ZROT_VAL * du;

    D3DXMatrixTranslation(&mat_trans, 0.0f, 0.0f, 5.0f);
    D3DXMatrixRotationYawPitchRoll(&mat_rot, D3DXToRadian(xrot), D3DXToRadian(yrot), D3DXToRadian(zrot));

    /* Get matrix transform from object to world coordinates */
    mat_world = *D3DXMatrixMultiply(&mat_world, &mat_rot, &mat_trans);
    IDirect3DDevice9_SetTransform(gd3d_pDevice, D3DTS_WORLD, &mat_world);

    /* Initialize scene, textures */
    IDirect3DDevice9_BeginScene(gd3d_pDevice);
    IDirect3DDevice9_SetTexture(gd3d_pDevice, 0, (IDirect3DBaseTexture9*)gd3d_pTexture);
    IDirect3DDevice9_SetStreamSource(gd3d_pDevice, 0, gd3d_pVertexBuffer, 0, sizeof(Vertex));
    IDirect3DDevice9_SetFVF(gd3d_pDevice, D3DFVF_CUSTOMVERTEX);

    /* Render polygons */
    IDirect3DDevice9_DrawPrimitive(gd3d_pDevice, D3DPT_TRIANGLESTRIP,  0, 2);
    IDirect3DDevice9_DrawPrimitive(gd3d_pDevice, D3DPT_TRIANGLESTRIP,  4, 2);
    IDirect3DDevice9_DrawPrimitive(gd3d_pDevice, D3DPT_TRIANGLESTRIP,  8, 2);
    IDirect3DDevice9_DrawPrimitive(gd3d_pDevice, D3DPT_TRIANGLESTRIP, 12, 2);
    IDirect3DDevice9_DrawPrimitive(gd3d_pDevice, D3DPT_TRIANGLESTRIP, 16, 2);
    IDirect3DDevice9_DrawPrimitive(gd3d_pDevice, D3DPT_TRIANGLESTRIP, 20, 2);

    /* Finish scene and render to screen */
    IDirect3DDevice9_EndScene(gd3d_pDevice);
    IDirect3DDevice9_Present(gd3d_pDevice, NULL, NULL, NULL, NULL);

}

void D3DTest_shutdown(void)
{
    if (gd3d_pTexture) IDirect3DTexture9_Release(gd3d_pTexture);
    if (gd3d_pVertexBuffer) IDirect3DTexture9_Release(gd3d_pVertexBuffer);
    if (gd3d_pDevice) IDirect3DTexture9_Release(gd3d_pDevice);
    if (gd3d_pD3D9) IDirect3DTexture9_Release(gd3d_pD3D9);
}

void D3DTest_testD3D(void)
{
    int retval;
    WNDCLASSEX wc = {0};
    MSG uMsg;
    /* Check whether test is running before continuing */
    /* TODO: Need to implement atomic locking! */
    if (gd3d_running)
        return;
    gd3d_running = TRUE;

    retval = MessageBox(NULL,"This will test Direct 3D 9 on this device. Okay to proceed?","Direct 3D Test",MB_YESNO);

    if (retval != IDYES)
    {
        gd3d_running = FALSE;
        return;
    }


    memset(&uMsg,0,sizeof(uMsg));
    wc.lpszClassName = "dxdiag";
    wc.cbSize = sizeof(WNDCLASSEX);
    wc.style = CS_HREDRAW | CS_VREDRAW;
    wc.lpfnWndProc = D3DTest_DlgProc;

    RegisterClassEx(&wc);

    gd3d_hWnd = CreateWindowEx(0, "dxdiag", "Direct 3D Test", WS_OVERLAPPEDWINDOW | WS_VISIBLE, 0, 0, 800, 600, 0, 0, 0, 0);

    ShowWindow(gd3d_hWnd, SW_SHOWNORMAL);
    UpdateWindow (gd3d_hWnd);

    D3DTest_initD3D();

    gd3d_t_elapsed = 0;
    gd3d_t_last = timeGetTime();

    while (uMsg.message != WM_QUIT)
        {
            if ( gd3d_t_elapsed >= TEST_TIMEOUT)
            {
                PostQuitMessage(0);
                DestroyWindow(gd3d_hWnd);
            }
            
            if (PeekMessage(&uMsg, NULL, 0, 0, PM_REMOVE))
            {
                TranslateMessage(&uMsg);
                DispatchMessage(&uMsg);
            }
            
            else
            {
                gd3d_t_cur = timeGetTime();
                gd3d_dt = gd3d_t_cur - gd3d_t_last;
                gd3d_t_last = gd3d_t_cur;
                gd3d_t_elapsed += gd3d_dt;
                D3DTest_render();
            }
        }
    D3DTest_shutdown();
    MessageBox(NULL,"Did you see the spinning box on the screen?","Direct 3D Test",MB_YESNO);
    UnregisterClass("dxdiag", wc.hInstance);
    gd3d_running = FALSE;
}

LRESULT CALLBACK D3DTest_DlgProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    switch(uMsg)
    {
        case WM_KEYDOWN:
        {
            switch(wParam)
            {
                case VK_ESCAPE:
                    PostQuitMessage(0);
                    DestroyWindow(hWnd);
                break;
            }
        }

        case WM_CLOSE:
            PostQuitMessage(0);
            DestroyWindow(hWnd);
        break;
        case WM_DESTROY:
            PostQuitMessage(0);
            DestroyWindow(hWnd);
        break;

        default:
            return DefWindowProc(hWnd, uMsg, wParam, lParam);
            break;
    }

    return 0;
}
